/*------------------------------------------------------------------------------
#
#	Apple Macintosh Developer Technical Support
#
#	Standard File Sample Application
#
#	StdFile
#
#	StdFile.c	-	C Source
#
#	Copyright © 1989 Apple Computer, Inc.
#	All rights reserved.
#
#	Versions:	
#				1.00				04/89
#
#	Components:
#				StdFile.c			April 1, 1989
#				StdFile.p			April 1, 1989
#				StdFile.h			April 1, 1988
#				StdFile.r			April 1, 1988
#				StdFile.rsrc		April 1, 1988
#				CStdFile.make		April 1, 1989
#				PStdFile.make		April 1, 1988
#

OBJECTIVES
----------
This program attempts to demonstrate the following techniques:

	- Normal use of SFGetFile and SFPutFile
	- Normal use of SFPGetFile and SFPPutFile. This includes use of Custom
		Dialogs with handling of extra items through the implementation of a
		DlgHook.
			- First time initialization
			- Extra Simple buttons (Quit/Directory/ThisDir)
			- Radio buttons (file format, types of files to show)
			- Aliasing click on some buttons to clicks on other buttons
			- Regenerating the list of files displayed
	- Creating a full pathname from the reply record (using Working Directories
		or DirID)
	- Selecting a directory (ala MPW's "GetFileName -d")
	- Simple file filter (checks file type)
	- Complex file filter (looking inside the file)
	- Adding and deleting and extra List Manager lists. This is shown for both
		SFGetFile and SFPutFile.
	- Select multiple files using one of two methods
		- Replace SF's list with one of your own
		- Add a second list to the Dialog Box. This method is not shown
			explicitly. Rather, I show how to install and dispose of the actual
			list item. Inserting filenames into the list is left as an exercise
			to you, the Programmer.
	- Setting the starting Directory/Volume
	- Describe pending update event clogging


NOTES
-----
		- This application assumes existance of HFS. It makes HFS calls and
			accesses HFS data structures without first checking to see if HFS
			exists on this machine.

------------------------------------------------------------------------------*/

#include <Types.h>
#include <QuickDraw.h>
#include <ToolUtils.h>
#include <Events.h>
#include <Controls.h>
#include <Windows.h>
#include <Dialogs.h>
#include <Menus.h>
#include <Desk.h>
#include <SegLoad.h>
#include <Files.h>
#include <OSEvents.h>
#include <Traps.h>
#include <Fonts.h>
#include <OSUtils.h>
#include <Resources.h>
#include <Memory.h>
#include <Packages.h>
#include <Lists.h>
#include <StdFile.h>

typedef struct StringHolder {
	struct StringHolder	**link;
	Str255	title;
} StringHolder, *StringHolderPtr, **StringHolderHdl;


#define SFSaveDisk		0x214		/* Negative of current volume refnum [WORD]	*/
#define CurDirStore		0x398		/* DirID of current directory [LONG]		*/
#define ResLoad			0xA5E		/* Boolean ResLoad setting [WORD]			*/
#define TopMapHndl		0xA50		/* 1st map in resource list [Handle]		*/


#define firstTime		-1  /* the first time our hook is called, it is passed a -1 */

#define reDrawList		101 /* returning 101 as item number will cause the FILE list
								to be recalculated */

#define BTNON			1   /* Control value for on */
#define BTNOFF			0   /* Control value for off */

#define HWCfgFlags		0xB22	/* Low memory global used to check if A/UX is running */


Boolean			doneFlag;		/* set to TRUE when time to quit */
SysEnvRec		theWorld;
Boolean			gHaveAUX;		/* set to TRUE if running under A/UX */
EventRecord		myEvent;
WindowPtr		whichWindow;
Boolean			WNEIsImplemented;
SFReply			reply;			/* used in all SF samples */
SFTypeList		typeList;		/* typelist for all SF samples */
OSErr			err;			/* used in all OS calls */
Point			gLocation = {0x40,0x40};
Point			gZeroPt = {0,0};

/* for normal SFPPutFile sample */
Boolean			textOnly;
short			MySaveDisk;
Rect			freeItemBox;
Str255			myStr, myStr2;

/* for SetDirectory sample */
long			MyCurDir;
Boolean			CurDirValid;

/* for MultiFile and ListsNPut samples */
ListHandle		LHandle;

/* for PutOptions sample */
short			OptionNumber;
DialogPtr		OptionsDPtr;

/* for MultiFile */
StringHolderHdl	firstName;
Boolean			namesChanged;
Boolean			needToUpdate;



/* Define HiWrd and LoWrd macros for efficiency. */

#define HiWrd(aLong)	(((aLong) >> 16) & 0xFFFF)
#define LoWrd(aLong)	((aLong) & 0xFFFF)

/* Define TopLeft and BotRight macros for convenience. Notice the implicit */
/* dependency on the ordering of fields within a Rect */

#define TopLeft(aRect)	(* (Point *) &(aRect).top)
#define BotRight(aRect)	(* (Point *) &(aRect).bottom)

/* _DataInit() is part of the MPW runtime library. This external */
/* reference to it is done so that we can unload its segment, %A5Init. */

extern void _DataInit();

void Initialize(void);
void EventLoop(void);
void DoCommand(long mResult);
void SetRadioButton(DialogPtr dialog,short item,short state);
void ShowAboutMeDialog(void);
char *pStrcat(char *s,char *t);
char *pStrcpy(char *s,char *t);
char *PathNameFromDirID(long DirID,short vRefNum,char *s);
char *PathNameFromWD(short vRefNum,char *s);
void ShowSelection(char *s);
void ShowCancelled(void);
void doNormalGet(void);
void doNormalPut(void);
void doNormalPGet(void);
void doNormalPPut(void);
void doFileFilter(void);
void doGetDirectory(void);
void doMultiFile(void);
void doListsNPutFile(void);
void doPutOptions(void);
void doIdleUpdates(void);
void doForceDirectory(void);


/* Let's do it... */

main()
{
	UnloadSeg((Ptr) _DataInit);		/* note that _DataInit must not be in Main! */

	MaxApplZone();					/* expand the heap so code segments load at the top */

	Initialize();					/* initialize the program */

	EventLoop();					/* call the main event loop */
}

Boolean TrapAvailable(tNumber,tType)
	short		tNumber;
	TrapType	tType;
{
	return NGetTrapAddress(tNumber, tType) != GetTrapAddress(_Unimplemented);
}

void Initialize()
{
	Handle		menuBar;
	short		*flagptr;

	InitGraf((Ptr) &qd.thePort);
	InitFonts();
	InitWindows();
	InitMenus();
	TEInit();
	InitDialogs(nil);
	InitCursor();

	WNEIsImplemented = TrapAvailable(_WaitNextEvent, ToolTrap);
	
	SysEnvirons(curSysEnvVers,&theWorld);
	flagptr = (short *) HWCfgFlags;
	if (*flagptr & (1 << 9))
	  	gHaveAUX = 1;			/* Do Have A/UX */
	else
		gHaveAUX = 0;			/* Don't have A/UX */

	doneFlag = false;
	MySaveDisk = *(short *)SFSaveDisk + 1;	/*so we're sure that they're different*/

	menuBar = GetNewMBar(rMenuBar);		/* read menus into menu bar */
	if (menuBar == nil) ExitToShell();
	SetMenuBar(menuBar);					/* install menus */
	DisposHandle(menuBar);
	AddResMenu(GetMHandle(mApple), 'DRVR');	/* add DA names to Apple menu */
	DrawMenuBar();

} /*Initialize*/


void EventLoop()
{
	Boolean			haveEvent;
	short			deskPart;		/* result from FindWindow */

	do {
		if ( WNEIsImplemented )
			haveEvent = WaitNextEvent(everyEvent, &myEvent, 0x7fffffff, nil);
		else {
			SystemTask();
			haveEvent = GetNextEvent(everyEvent, &myEvent);
		}

		deskPart = FindWindow(myEvent.where, &whichWindow);
		if ( deskPart != inSysWindow ) InitCursor();

		if (haveEvent) {
			switch (myEvent.what) {
				case mouseDown:
					switch (deskPart) {
						case inSysWindow:
							SystemClick(&myEvent, whichWindow);
							break;
						case inMenuBar:
							DoCommand(MenuSelect(myEvent.where));
							break;
					}
					break;

				case keyDown:
				case autoKey:
					if ((myEvent.modifiers&cmdKey) != 0) {
						DoCommand(MenuKey(myEvent.message&charCodeMask));
					}
					break;
			}
		}
	}
	while (!doneFlag);
}


/************************ Performed the selected command *************************/

void DoCommand(mResult)
	long	mResult;
{
	short	theItem;
	short	theMenu;
	Str255	name;
	GrafPtr	oldPort;

	theItem = LoWrd(mResult);
	theMenu = HiWrd(mResult);

	switch (theMenu) {

		case mApple:
			if (theItem == iAboutMe) {
				ShowAboutMeDialog();
			} else {
				GetItem(GetMHandle(mApple),theItem,&name);
				GetPort(&oldPort);
				OpenDeskAcc(&name);
				SetPort(oldPort);
			};
			break;
		case mFile:
			switch (theItem) {
				case iQuit:
					doneFlag = true;
					break;
			};
			break;
		case mEdit:
			SystemEdit(theItem-1);
			break;
		case mSFile:
			switch (theItem) {
				case iNormalGet:		doNormalGet();		break;
				case iNormalPut:		doNormalPut();		break;
				case iNormalPGet:		doNormalPGet();		break;
				case iNormalPPut:		doNormalPPut();		break;
				case iFileFilter:		doFileFilter();		break;
				case iGetDirectory:		doGetDirectory();	break;
				case iMultiFile:		doMultiFile();		break;
				case iListsNPutFile:	doListsNPutFile();	break;
				case iPutOptions:		doPutOptions();		break;
				case iIdleUpdates:		doIdleUpdates();	break;
				case iForceDirectory:	doForceDirectory();	break;
			};
			break;
	};
	HiliteMenu(0);
};



/** SetRadioButton ************************************************************/
/*
/*	Handy routine for setting the value of a radio button. Given a dialog
/*	pointer, and item number, and a state, this routine will take care of
/*	the rest.
/*
/******************************************************************************/

void SetRadioButton(dialog,item,state)
	DialogPtr	dialog;
	short		item;
	short		state;
{
	short		kind;
	Handle		h;
	Rect		r;

	GetDItem(dialog,item,&kind,&h,&r);
	SetCtlValue((ControlHandle)h,state);
}


/** ShowAboutMeDialog *********************************************************/
/*
/*	Shows the obligatory vanity box. Normally shows the standard boring
/*	Developer Technical Support About box, but can be enlivened by holding
/*	the Command key when making the menu selection. Rick Blair says that in the
/*	future, we'll have better ones...
/*
/******************************************************************************/

void ShowAboutMeDialog()
{
	if ((myEvent.modifiers & cmdKey) == 0) {
		Alert(rAboutMeDLOG, nil);
	} else {
		Alert(rRealAboutMeDLOG, nil);
	}
}


/** pStrcat / pStrCpy *********************************************************/
/*
/*	A couple of utility routines. C is thoughtless enough to not really 
/*	support P-strings. In order to perform string copies and concatenations,
/*	I have to use these routines.
/*
/******************************************************************************/

char *pStrcat(s,t)
	unsigned char *s, *t;
{
	unsigned char *s2;
	short tLen;

	s2 = s + *s;
	*s += (tLen = *t);
	for (++tLen; --tLen; s2[tLen] = t[tLen]);
	return (s);
}

char *pStrcpy(s,t)
	unsigned char *s, *t;
{
	short	tLen;

	for (tLen = *t + 1; tLen--; s[tLen] = t[tLen]);
	return (s);
}


/** PathNameFromDirID *********************************************************/
/*
/*	Given a DirID and real vRefnum, this routine will create and return the
/*	full pathname that corresponds to it. It does this by calling PBGetCatInfo
/*	for the given directory, and finding out its name and the DirID of its
/*	parent. It the performs the same operation on the parent, sticking ITS
/*	name onto the beginning of the first directory. This whole process is
/*	carried out until we have processed the root directory (identified with
/*	a DirID of 2.
/*
/*	NOTE: This routine is now A/UX friendly. A/UX likes sub-directories
/*			separated by slashes in a pathname. This routine automatically
/*			uses colons or slashes as separators based on the value of the
/*			global gHasAUX.  This global must be initialized correctly for
/*			this routine to do its thing. However, because of this dependancy
/*			on the idiosyncracies of file systems, generating full pathnames
/*			for other than display purposes is discouraged; it's changed in
/*			the past when A/UX was implemented, and it may change again in
/*			the future it support for other file systems such as ProDOS,
/*			MS-DOS, or OS/2 are added.
/*
/******************************************************************************/

char *PathNameFromDirID(DirID, vRefNum, s)
	long	DirID;
	short	vRefNum;
	char	*s;
{
	CInfoPBRec	block;
	Str255		directoryName;

	*s = 0;
	block.dirInfo.ioNamePtr = &directoryName;
	block.dirInfo.ioDrParID = DirID;

	do {
		block.dirInfo.ioVRefNum = vRefNum;
		block.dirInfo.ioFDirIndex = -1;
		block.dirInfo.ioDrDirID = block.dirInfo.ioDrParID;

		err = PBGetCatInfo(&block,false);
		if (gHaveAUX) {
			if (directoryName[1] != '/')
				/* If this isn't root (i.e. '/'), append a slash ('/') */
				pStrcat(&directoryName,"\p/");
		} else 
			/* Append a Macintosh style colon (':') */
			pStrcat(&directoryName,"\p:");
		pStrcat(&directoryName,s);
		pStrcpy(s,&directoryName);
	} while (block.dirInfo.ioDrDirID != 2);

	return(s);
}


/** PathNameFromWD ************************************************************/
/*
/*	Given an HFS working directory, this routine returns the full pathname
/*	that corresponds to it. It does this by calling PBGetWDInfo to get the
/*	VRefNum and DirID of the real directory. It then calls PathNameFromDirID,
/*	and returns its result.
/*
/******************************************************************************/

char *PathNameFromWD(vRefNum,s)
	long	vRefNum;
	char	*s;
{

	WDPBRec	myBlock;

	/*
	/* PBGetWDInfo has a bug under A/UX 1.1.  If vRefNum is a real vRefNum
	/* and not a wdRefNum, then it returns garbage.  Since A/UX has only 1
	/* volume (in the Macintosh sense) and only 1 root directory, this can
	/* occur only when a file has been selected in the root directory (/).
	/* So we look for this and hardcode the DirID and vRefNum. */

	if (gHaveAUX && (vRefNum == -1))
		return(PathNameFromDirID(2,-1,s));

	myBlock.ioNamePtr = nil;
	myBlock.ioVRefNum = vRefNum;
	myBlock.ioWDIndex = 0;
	myBlock.ioWDProcID = 0;

	/* Change the Working Directory number in vRefnum into a real vRefnum */
	/* and DirID. The real vRefnum is returned in ioVRefnum, and the real */
	/* DirID is returned in ioWDDirID. */

	PBGetWDInfo(&myBlock,false);

	return(PathNameFromDirID(myBlock.ioWDDirID,myBlock.ioWDVRefNum,s));
};


/** ShowSelection *************************************************************/
/*
/*	This routine accepts a string as input, prepends STR#(256,4) - which
/*	should be "The item selected was " - and shows a NoteAlert with the
/*	result.
/*
/******************************************************************************/

void ShowSelection(s)
	char *s;
{
	Str255 tStr;

	GetIndString(&tStr,rStrMisc,4);
	ParamText(pStrcat(&tStr,s),nil,nil,nil);
	NoteAlert(rShowSelectionDLOG, nil);
};


/** ShowCancelled *************************************************************/
/*
/*	Shows STR#(256,5) in a NoteAlert. This string says in English "The Cancel
/*	button was pressed."
/*														- Douglas Hofstadter
/*
/******************************************************************************/

void ShowCancelled()
{
	Str255	tStr;

	GetIndString(&tStr,rStrMisc,5);
	ParamText(&tStr,nil,nil,nil);
	NoteAlert(rShowSelectionDLOG,nil);
};


/** doNormalGet ***************************************************************/
/*
/*	Simplest form of SFGetFile. This routine puts up a GetFile dialog with
/*	a request to show all files. When this is done, a check is made to see
/*	which of the Save or Cancel buttons were pressed. If the Save button was
/*	pressed, ShowSelection is called to display which file was selected. If the
/*	Cancel button was pressed, a dialog is shown saying so.
/*
/*	NOTE: The second parameter - the prompt string - is ignored in SFGetFile
/*			calls. As noted on page I-523 of Inside Mac, it is there for
/*			historical reasons only.
/*
/******************************************************************************/

void doNormalGet()
{
	Str255	s;

	SFGetFile(gLocation,			/* location */
		"\pSpace for Rent",			/* vestigial string */
		nil,						/* fileFilter */
		-1,							/* numtypes; -1 means all */
		&typeList,					/* array to types to show */
		nil,						/* dlgHook */
		&reply);					/* record for returned values */

	if (reply.good)
		ShowSelection(pStrcat(PathNameFromWD(reply.vRefNum,&s),&reply.fName));
	else
		ShowCancelled();
};


/** doNormalPut ***************************************************************/
/*
/*	Simplest form of SFPutFile. This routine puts up a PutFile dialog with a
/*	prompt and suggested file name.
/*
/******************************************************************************/

void doNormalPut()

{
	Str255	s;

	SFPutFile(gLocation,			/* location */
		"\pSave document as:",		/* prompt string */
		"\pDoug",					/* original name */
		nil,						/* dlgHook */
		&reply);					/* record for returned values */

	if (reply.good)
		ShowSelection(pStrcat(PathNameFromWD(reply.vRefNum,&s),reply.fName));
	else
		ShowCancelled();
};


/** doNormalPGet **************************************************************/
/*
/*	SFPGetFile with Dialog Hook and Simple File Filter. Depending on the value
/*	of the Global variable "textOnly", it shows either TEXT files, or TEXT and
/*	APPL files. The value of "textOnly" is determined by the states of two
/*	radio buttons that are added to the dialog box. Our dlgHook routine is used
/*	to initialize the radio buttons and handle hits on them. When there is a
/*	hit on a radio button, "textOnly" is set to either TRUE or FALSE, and a
/*	special command is sent back to Standard File, telling it to regenerate
/*	the list of file names it is displaying. This sample consists of 3 parts:
/*
/*	doNormalPGet - Called by doCommand. This routine initializes a variable
/*		for our file filter, and then calls SFPGetFile with pointers to
/*		two other routines and a resource number for a special dialog box
/*		with extra items in it.
/*
/*	SimpleFileFilter - Specified in our SFPGetFile call to be called to
/*		specify whether or not a file should be displayed. All TEXT files
/*		are displayed. If the global variable 'textOnly' is FALSE, then
/*		applications (APPL files) are also displayed. NO other files are
/*		displayed.
/*
/*	MySFGetHook - A routine that is called to handle hits on the non-standard
/*		items in our dialog box. These items are:
/*			- 2 radio buttons that affect the setting of the 'textOnly'
/*				variable,
/*			- a Quit button that causes the GetFile dialog box to be put
/*				away (like pressing Cancel), but also causes this demo to be
/*				exitted.
/*
/*		The dlgHook is also used to perform some special initialization
/*		on the items in the dialog box. Standard file does this by calling
/*		this routine with a bogus 'item' number of -1. When we get this
/*		number, we initialize our radio buttons, and change the text in the
/*		Open button.
/*
/*		The radio buttons are used to determine what files will appear in
/*		the files list. It does this by apporpriately setting the 'textOnly'
/*		variable for SimpleFileFilter, and then telling Standard File that
/*		the file list needs to be regenerated by passing back 101 as the
/*		the function result.
/*
/*		The Quit button causes the dialog box to go away by returning the
/*		'getCancel' to Standard File. It also sets the 'doneFlag' variable
/*		to TRUE for our MainEventLoop to notice.
/*
/******************************************************************************/

pascal short MySFGetHook(MySFItem, theDialog)
	short		MySFItem;
	DialogPtr	theDialog;
{
#define textButton 11
#define textAppButton 12
#define quitButton 13

	Handle		itemToChange;	/* needed for GetDItem and SetCtlValue */
	Rect		itemBox;		/* needed for GetDItem */
	short		itemType;		/* needed for GetDItem */
	Str255		buttonTitle;	/* needed for GetIndString */

	/* MySFGetHook is a function that requires that an item number be passed	*/
	/* back from it. Normally, this is the same item number that was passed		*/
	/* to us, but not necessarily. For instance, clicks on the Quit button		*/
	/* get translated into clicks on the Cancel button. We could also return	*/
	/* values that cause the file names to be redrawn or have the whole event	*/
	/* ignored. However, by default, we'll return what was sent to us.			*/

	switch (MySFItem) {
		case firstTime:
		/* Before the dialog is drawn, our hook gets called with a -1. */
		/* This gives us the opportunity to change things like Button titles, etc. */

		/* Set the textAppButton to OFF, the textButton to ON */

			SetRadioButton(theDialog,textAppButton,BTNOFF);
			SetRadioButton(theDialog,textButton,BTNON);

		/* Get the Button title from the resource fork. If we can't get the */
		/* resource, we just won't change the open Button's title*/

			GetIndString(&buttonTitle,rStrMisc,1);
			if (*buttonTitle != 0) { /* if we really got the resource */
				GetDItem(theDialog,getOpen,&itemType,&itemToChange,&itemBox);
				SetCTitle((ControlHandle)itemToChange,&buttonTitle);
			};
			break;

		case textButton:

		/* Turn the textAppButton OFF, the textButton ON and redraw the list*/

			if (!textOnly) {
				SetRadioButton(theDialog,textAppButton,BTNOFF);
				SetRadioButton(theDialog,textButton,BTNON);
				textOnly = true;
				return(reDrawList); /* we must tell SF to redraw the list */
			};
			break;

		case textAppButton:

		/* Turn the textButton OFF, the textAppButton ON and redraw the list*/

			if (textOnly) {
				SetRadioButton(theDialog,textButton,BTNOFF);
				SetRadioButton(theDialog,textAppButton,BTNON);
				textOnly = false;
				return(reDrawList); /* we must tell SF to redraw the list */
			};
			break;

		case quitButton:

		/* Alias clicks on the Quit button to clicks on the Cancel Button. Also, */
		/* set 'doneFlag' to TRUE so that the MainEventLoop terminates. */

			doneFlag = true;
			return (getCancel); /* Pass SF back a 'cancel Button' */

		default:
			return(MySFItem);
	};
};

pascal Boolean SimpleFileFilter(p)
	ParmBlkPtr p;
{

	/* If we have a 'TEXT' file, or we have an 'APPL' file and 'textOnly' is */
	/* FALSE, then signify that we should show the file by returning FALSE */

	if ((p->fileParam.ioFlFndrInfo.fdType == 'TEXT') ||
		((!textOnly) && (p->fileParam.ioFlFndrInfo.fdType == 'APPL')))
			return(false);
	else
		return(true);
}


void doNormalPGet()
{
	textOnly = true;
	SFPGetFile(gLocation,	/* location */
		"\pSpace for Rent",	/* vestigial string */
		SimpleFileFilter,	/* fileFilter */
		-1,					/* numtypes; -1 means all */
		&typeList,			/* array to types to show */
		MySFGetHook,		/* dlgHook */
		&reply,				/* record for returned values */
		rSFPGetFileDLOG,	/* ID of Custom Dialog */
		nil);				/* ModalDialog filterProc */
};

/** doNormalPPut **************************************************************/
/*
/*	Normal SFPPutFile sample. This custom PutFile routine adds several new
/*	to the dialog box. There are 2 radio buttons that determine what format
/*	the user wants to save the information in. There is a Quit buttons that
/*	will cause us to leave this program. And there is a Static Text item that
/*	displays the amount of free space left on the disk. This text item is
/*	updated whenever we detect that the volume we are currently looking at is
/*	not the same as the last volume we looked at. Therefore, we maintain a
/*	variable called MySaveDisk to track this. This sample consists of 3 parts:
/*
/*	doNormalPPut - This routine, called by doCommand, calls SFPPutFile. Note
/*		the extra 'P' in the name. This toolbox call allows us to specify a
/*		customized dialog box, and dlgHook that handles hits on special
/*		items.
/*
/*	MySFPutHook - A routine that is called to handle the non-standard items
/*		in our dialog box. These items are:
/*			- 2 radio buttons that affect the setting of the 'textOnly'
/*				variable,
/*			- a Quit button that causes the GetFile dialog box to be put
/*				away (like pressing Cancel), but also causes this demo to be
/*				exitted,
/*			- a UserItem that displays the amount of free space on the
/*				current volume.
/*
/*		The dlgHook is also used to perform some special initialization
/*		on the items in the dialog box. Standard file does this by calling
/*		this routine with a bogus 'item' number of -1. When we get this
/*		number, we initialize our radio buttons, change the text in the
/*		save button, and prepare the user item by initializing the routine
/*		that will draw it and getting the text that will appear in it.
/*
/*		The radio buttons are used to determine what format the file will be
/*		saved in. They are functionless in this sample, merely changing
/*		their state and setting a global variable for possible use later.
/*
/*		The Quit button causes the dialog box to go away by returning the
/*		'putCancel' to Standard File. It also sets the 'doneFlag' variable
/*		to TRUE for our MainEventLoop to notice.
/*
/*		Finally, this routine performs one other function. When SFPPutFile
/*		is called, we make note of the current volume (the one that will
/*		be displayed in the dialog box). This routine is responsible for
/*		checking to see whenever we change volumes, and to update the
/*		Free Space notice accordingly. It does this by looking at the value
/*		stored at SFSaveDisk. Whenever that value changes, we have changed
/*		volumes and need to make a PBHGetVInfo call to find out how much
/*		room is left on the disk.
/*
/*	DrawFreeSpaceItem - used to draw a user item that displays the amount of
/*		free space left on the disk.
/*
/******************************************************************************/

pascal void DrawFreeSpaceItem(theWindow, itemNo)
	WindowPtr	theWindow;
	short		itemNo;
{
#pragma unused (theWindow,itemNo)
	TextBox(myStr + 1, *myStr, &freeItemBox, teJustCenter);
};


pascal short MySFPutHook(MySFItem, theDialog)
	short		MySFItem;
	DialogPtr	theDialog;
{
#define quitButton 9
#define textButton 10
#define formatButton 11
#define freeSpaceItem 12  /* DITL item number of free space static text */

	Handle			itemToChange;	/* needed for GetDItem and SetCtlValue */
	short			itemType;		/* needed for GetDItem */
	Rect			itemBox;		/* needed for GetDItem */
	HParamBlockRec	myHPB;



/* MySFPutHook is a function that requires that an item number be passed	*/
/* back from it. Normally, this is the same item number that was passed		*/
/* to us, but not necessarily. For instance, clicks on the Quit button		*/
/* get translated into clicks on the Cancel button. We could also return	*/
/* values that cause the file names to be redrawn or have the whole event	*/
/* ignored. However, by default, we'll return what was sent to us.			*/

/* Before the dialog is drawn, our hook gets called with a -1. */
/* This gives us the opportunity to change things like Button titles, etc. */

	if (MySFItem == firstTime) {

	/* Set the FormatButton to ON, the textButton to OFF*/

		SetRadioButton(theDialog,formatButton,BTNON);
		SetRadioButton(theDialog,textButton,BTNOFF);
		textOnly = false;

	/* Get the text for the Save button from the resource fork. */
	/* If we can't get the resource, we just won't change the Button's title*/

		GetIndString(&myStr,rStrMisc,1);
		if (*myStr != 0) {			/*if we really got the resource*/
			GetDItem(theDialog,putSave,&itemType,&itemToChange,&itemBox);
			SetCTitle((ControlHandle)itemToChange,&myStr);
		};

	/* Set up our routine to draw our user item */

		GetDItem(theDialog,freeSpaceItem,&itemType,&itemToChange,&freeItemBox);
		SetDItem(theDialog,freeSpaceItem,itemType,(Handle)DrawFreeSpaceItem,
				 &freeItemBox);

	/* Get the text to be used  in our user item. */

		GetIndString(&myStr2,rStrMisc,2);

	}; /* if MySFItem == firstTime */

/* Check to see if we have changed drives. If so, then we need to update */
/* the text in our UserItem. */

	if (MySaveDisk != *(short *)SFSaveDisk) {
		/* set up the block for the PBHGetVInfo call*/
		myHPB.volumeParam.ioNamePtr = nil;			/* we don't care about the name */
		myHPB.volumeParam.ioVRefNum = -*(short *)SFSaveDisk;
		myHPB.volumeParam.ioVolIndex = 0;			/* use ioVRefNum only */

		err = PBHGetVInfo(&myHPB,false);

		if (err == noErr) {
			NumToString(((long)myHPB.volumeParam.ioVFrBlk * myHPB.volumeParam.ioVAlBlkSiz) / 1024,&myStr);
		} else  /* Handle error - we can't get the free space size! */
			pStrcpy(myStr, "\p??");

	/* Add the file size to 'K free' (or whatever) */

		pStrcat(&myStr,&myStr2);

	/* Draw the new freeBlocks in our userItem */

		DrawFreeSpaceItem(theDialog,freeSpaceItem);

	/* SFSaveDisk changed, so we need to update mySaveDisk*/

		MySaveDisk = *(short *)SFSaveDisk;

	}; /*IF mySaveDisk != SFSaveDisk^*/

	switch(MySFItem) {

		case textButton:

		/* Turn the FormatButton OFF, the textButton ON */

			if (!textOnly) {
				SetRadioButton(theDialog,formatButton,BTNOFF);
				SetRadioButton(theDialog,textButton,BTNON);
				textOnly = true; /* change our flag accordingly */
			};	/*if not textOnly*/
			break;

		case formatButton:

		/* Turn the textButton OFF, the FormatButton ON */

			if (textOnly) {
				SetRadioButton(theDialog,textButton,BTNOFF);
				SetRadioButton(theDialog,formatButton,BTNON);
				textOnly = false; /* change our flag accordingly */
			};	/*if not textOnly*/
			break;

		case quitButton:
			doneFlag = true;
			return(putCancel); /* Pass SF back a 'cancel Button' */

	};	/* switch */
	return(MySFItem);
};


void doNormalPPut()
{
	SFPPutFile(gLocation,		/* location */
		"\pSave document as:",	/* prompt string */
		"\pDoug",				/* original name */
		MySFPutHook,			/* dlgHook */
		&reply,					/* record for returned values */
		rSFPPutFileDLOG,		/* ID of custom Dialog */
		nil);					/* ModalDialog filterProc */
};


/** ComplexFileFilter *********************************************************/
/*
/*	Sample of a less trivial file filter. This routine is responsible for
/*	displaying only files that have 'ALRT' resources.  It shows how to
/*	correctly look inside a file for qualifying information, and demonstrates
/*	how to get around a particular quirk when attempting multiple access to
/*	the resource fork.
/*
/*	In order to save time, the first thing we do is turn off resource loading;
/*	we just want to count the number of ALRT resources, and don't care about
/*	the actual data itself. So I save off the current ResLoad setting, and
/*	then set it to FALSE.
/*
/*	I then prepare to open the resource fork of the file. However, I can't
/*	simply call OpenResFile. OpenResFile only works for files that are in
/*	the default directory. Standard file makes HFS calls when it can, and
/*	explicitly uses DirIDs when getting file names and other information.
/*	Therefore, I can't count on the default directory being set to the dir-
/*	ectory that we are examining. In order to call OpenResFile, I have to set
/*	the default directory.
/*
/*	So I take the VRefNum and DirID passed to me, and I create a Working
/*	Directory from them. I save the old default directory, and then set it
/*	to the WD I just created.
/*
/*	So far, so good. I am now ready to open the resource fork of the file,
/*	call Count1Resources (which limits enumeration to the current file only,
/*	excluding other files in the resource chain), and close the file. But
/*	wait! I must make sure that I am not closing a file that I don't really
/*	want to close. For example, one of the files I may be examining may be
/*	myself. If I were to blindly close that file, I would be closing my CODE
/*	resources, removing them from memory. Empirical experience has told me that
/*	this is not good.
/*
/*	So I have to make sure that I don't close my own resource fork. I could
/*	possibly check the refnum that I get back from OpenResFile against one
/*	that I could get by calling CurResFile at the start of the program. If
/*	they match, then I am looking at my own file, and I shouldn't close it. But
/*	this doesn't work always. For instance, I also want to make sure that I
/*	don't close the System Resource file when I point and click my way into
/*	the System Folder. What if there are DA's open? I don't want to close them.
/*	How about the Fonts/DA's file used by Suitcase? What about the DeskTop
/*	file? Basically, I need a way to determine on the fly if a resource file
/*	is already open before I try to open it myself.
/*
/*	Well, there is a way. Within the parameter block passed to me is a field
/*	called ioFlAttrib. It is described on page IV-122 of Inside Mac. As
/*	described there, bit 2 contains the state of the resource fork. If the fork
/*	is open, then the bit is set. All that is necessary for me to do is check
/*	the state of this bit before I open the file. If the bit is clear, then
/*	I need to close the file when I am done.
/*
/*	But wait! There's more. This technique - alas! - will not work under
/*	MultiFinder! Why? Because this bit is set even if another program in
/*	another partition has the resource fork open. I would test that bit, find
/*	that the file is open, open the file for myself, count the number of
/*	resources, and then leave the file open. This now leaves another resource
/*	file in the resource chain - one that wasn't there before and that
/*	shouldn't be there now. Another technique is needed.
/*
/*	Fortunately, one is at hand. On page I-115 of Inside Mac is a brief
/*	description of a low-memory variable called TopMapHndl. This contains
/*	a handle to the resource map of the first resource file in the chain.
/*	Since a resource file is made the current resource file when OpenResFile
/*	is called if it is actually being opened, we can test the value of
/*	TopMapHndl to see if the file was really opened. If the value of TopMapHndl
/*	changes after OpenResFile, then the resource fork was open for this
/*	application, and should be closed when we are done with it. If TopMapHndl
/*	doesn't change, then no new resource maps were added to the chain; no
/*	new files were opened. Therefore, the file doesn't need to be closed later.
/*	So far, this solution seems to be the best.
/*
/*	After doing all of this, we are almost done. All that needs to be done is
/*	restore the default directory and the value of ResLoad to their previous
/*	settings. When we have done so, we can return to Standard File.
/*
/******************************************************************************/

pascal Boolean ComplexFileFilter(p)
	ParmBlkPtr	p;
{
	short			refnum, tRefNum;
	WDPBRec			WDRec;
	ParamBlockRec	oldVol, newVol;
	Boolean			oldResLoad, closeIt;
	Handle			tHandle;
	Boolean			returnVal;

	returnVal = true;		/* Don't show it -- default */

/* Save the current setting of ResLoad. Then set it to FALSE. */

	oldResLoad = *(Boolean *)ResLoad;
	SetResLoad(false);

/* Save the current default directory/volume. */

	oldVol.volumeParam.ioNamePtr = nil;
	PBGetVol(&oldVol,false);

/* Create a working directory for the directory we are examining. */

	WDRec.ioNamePtr = nil;
	WDRec.ioVRefNum = p->fileParam.ioVRefNum;
	WDRec.ioWDProcID = 'ERIK';
	WDRec.ioWDDirID = *(long *)CurDirStore;

	PBOpenWD(&WDRec,false);

/* Set the Working Directory we just created to be the default. */

	newVol.volumeParam.ioNamePtr = nil;
	newVol.volumeParam.ioVRefNum = WDRec.ioVRefNum;

	PBSetVol(&newVol,false);

/* Check the current value of TopMapHndl, open the resource file, and */
/* check it again. If it changed, then note that we should close this */
/* file later when we are done with it. */

	tHandle = *(Handle *)TopMapHndl;
	refnum = OpenResFile(p->fileParam.ioNamePtr);
	if (tHandle != *(Handle *)TopMapHndl)
		closeIt = true;
	else
		closeIt = false;

/* If we successfully opened the file, then count the number of ALRT	*/
/* resources in it. Call UseResFile to make sure that the file we just	*/
/* 'opened' is the current one. We have to do this, as OpenResFile will	*/
/* not make the file the current one if it is already open. So save the	*/
/* current resFile, (possibly) change it to the one we want, read the	*/
/* number of ALRT resources in it, and then set the current resource	*/
/* file back to what it was. Finally, if we need to close the file we	*/
/* opened, do so. */

	if (ResError() == noErr) {
		tRefNum = CurResFile();
		UseResFile(refnum);
		if (Count1Resources('ALRT') > 0) returnVal = false;
		UseResFile(tRefNum);
		if (closeIt) CloseResFile(refnum);
	};

/* All done! Reset the default directory and ResLoad, and then blow. */

	PBSetVol(&oldVol,false);
	PBCloseWD(&WDRec,false);
	SetResLoad(oldResLoad);

	return(returnVal);
};

void doFileFilter()
{
	SFGetFile(gLocation,	/* location */
		"\pSpace for Rent",	/* vestigial string */
		ComplexFileFilter,	/* fileFilter */
		-1,					/* numtypes; -1 means all */
		&typeList,			/* array to types to show */
		nil,				/* dlgHook */
		&reply);			/* record for returned values */
};


/** doGetDirectory ************************************************************/
/*
/*	Shows how to modify SFGetFile to allow you to select a directory. This
/*	mimics the "GetFileName -d" function of MPW. As a matter of fact, the
/*	DLOG and DITL used in this sample are taken directly out of MPW.
/*
/*	There are 2 major additions in the dialog used in this sample:
/*		- a Simple button that lets one select the directory that is
/*			currently highlighted in the list of directories,
/*		- a Simple button at the top of the dialog that lets the user select
/*			the directory that we are currently *IN*.
/*
/*	No new techniques are really used in this sample. Hits on the two simple
/*	buttons are handled by a dlgHook called MyGetDirHook. Depending on
/*	which button is hit, we set the global variable MyCurDir to either
/*	reply.fType (for the currently highlighted directory) or CurDirStore^ (for
/*	the directory that we are currently in). We then simulate a hit on the
/*	Cancel button so that Standard File will return to our application.
/*
/*	However, when we get back to our application, there is no way for it to
/*	determine if we simulated a click on the Cancel Button, or if the user
/*	actually clicked on it; reply.good will be FALSE in either case. So we also
/*	set the value of global variable CurDirValid to TRUE if we only simulated
/*	a click on Cancel.
/*
/*	We also use the "Item== -1" feature to set up a prompt string and init-
/*	alize the value of CurDirValid to FALSE.
/*
/******************************************************************************/
pascal short MyGetDirHook(item, dPtr)
	short		item;
	DialogPtr	dPtr;
{
/* Equates for the itmes that I've added */
#define getDirButton 11
#define getDirNowButton 12
#define getDirMessage 13

	Str255	messageTitle;
	short	kind;
	Handle	h;
	Rect	r;

	switch (item) {
		case firstTime:

			/* Read in the prompt string from the resource fork, and initialize */
			/* CurDirValid to FALSE. */

			GetIndString(&messageTitle,rStrMisc,3);
			GetDItem(dPtr,getDirMessage,&kind,&h,&r);
			SetIText(h,&messageTitle);
			CurDirValid = false;
			break;
		case getDirButton:
			if (reply.fType != 0) {
				MyCurDir = reply.fType;
				CurDirValid = true;
				return(getCancel);
			};
			break;
		case getDirNowButton:
			MyCurDir = *(long *)CurDirStore;
			CurDirValid = true;
			return(getCancel);
	};
	return(item);	/* By default, return the item passed to us. */
};


pascal Boolean FoldersOnly(p)
	ParmBlkPtr p;
{
	/* Normally, folders are ALWAYS shown, and aren't even passed to		*/
	/* this file filter for judgement. Under such circumstances, it is		*/
	/* only necessary to blindly return TRUE (allow no files whatsoever).	*/
	/* However, Standard File is not documented in such a manner, and		*/
	/* this feature may not be TRUE in the future. Therefore, we DO check	*/
	/* to see if the entry passed to us describes a file or a directory.	*/

	if ((p->fileParam.ioFlAttrib & 0x10) != 0)
		return(false);
	return(true);
};


void doGetDirectory()
{
	Str255	s;

	SFPGetFile(gLocation,	/* location */
		"\pSpace for Rent",	/* vestigial string */
		FoldersOnly,		/* fileFilter */
		-1,					/* numtypes; -1 means all */
		&typeList,			/* array to types to show */
		MyGetDirHook,		/* dlgHook */
		&reply,				/* record for returned values */
		rGetDirectoryDLOG,
		nil);

	if (CurDirValid)
		ShowSelection(PathNameFromDirID(MyCurDir,-*(short *)SFSaveDisk,&s));
};


/** MultiFile *****************************************************************/
/*
/*	WARNING! The following is an experiment that failed! I was playing around
/*			with different ways for implementing a facility for multiple
/*			file handling. This was one of them. As with all experiments that
/*			fail, it's pretty ugly. The following should NOT be used as a
/*			sample on how to do things right, but as a lesson on how NOT to
/*			do things.
/*
/*	This sample is an attempt to implement the facilty to let the user choose
/*	more than one file at once. It does this by remembering all of the files
/*	passed to the fileFilter, and using those names to create a new, application
/*	governed filename list that will replace the one displayed by Standard File.
/*	However, this experiment seems doomed to failure, for the following reasons:
/*
/*		- Icons are not displayed next to the names of files. The resulting
/*			presentation is more disconcerting that I thought.
/*		- The current directory button (the one displayed above the list of
/*			filenames) disappears. This seems due to the fact that that pop-up
/*			menu-like item is not actually a dialog item, and is somehow attached
/*			to the list of filenames handled by Standard File. Since I move that
/*			list off of the window so that I can display my own, the current
/*			directory button seems to go with it.
/*		- Updating the list is difficult. The normal sequence of events would
/*			desirably be: 1) have the fileFilter create a linked list of names
/*			to appear in the dialog, 2) insert those names into the List Manager
/*			list, and 3) display the list. However, by following this outine,
/*			the list does not get updated, as the update region for that area
/*			is empty. So, at some time, we need to invalidate the area
/*			that holds our list. But there is no convenient place in which to
/*			do that. We can't call InvalRect within our fileFilter routine, as
/*			we don't have the DialogPtr at the time (needed to get the bounding
/*			rectangle of the list item). Neither can we call InvalRect within
/*			step 2, as we are between BeginUpdate/EndUpdates, and calling
/*			InvalRect would be useless.
/*		- We can't display directories. They are not offered to the judgement
/*			of our fileFilter, and hence cannot be added to our list.
/*		- Key presses would have do be handled manually by our dlgHook.
/*
/*	Our thesis is implemented as described, with all of the above limitations.
/*	What follows is a description of the routines used:
/*
/*	ScoopNames - The fileFilter routine that is responsible for remembering
/*		the names of all the files passed to it by Standard file. The names
/*		are stored in a singly linked list of handles. Each handle contains
/*		a Str255 to hold the name, and room for the handle for the next name.
/*		The root of the list is stored in the global variable 'firstName'.
/*		The list is used by SetNames to set the data for the list cells.
/*
/*	MultiFileHook - Creates the list and installs it as a user item. Also,
/*		we check on NULL events to see if our list needs to be updated. If
/*		so, we call InvalRect to force it to be redrawn. Finally, this
/*		routine disposes of the list if Open or Cancel are pressed.
/*
/*	MFModalFilter - Calls LClick for MouseDowns on the list.
/*
/*	MFDrawList - Redraws the list. If there are new names to be displayed, then
/*		they are inserted into the cells. Then LUpdate is called to redraw
/*		the list.
/*
/*	SetNames - Called by MFDrawList when FirstName is != NIL, indicating that
/*		there are new names to be displayed. We first get a count of the
/*		number of names in the linked list, and the List Manager list is
/*		adjusted accordingly. Then, the linked list is traversed, with each
/*		name being inserted into its appropriate cell. The nodes of the
/*		linked list are disposed of along the way. When everything is done,
/*		the List Manager list is appropriately set up, and the linked list is
/*		disposed of, with FirstName== NIL.
/*
/******************************************************************************/


void SetNames()
{
	StringHolderHdl	nextName;
	short			count;
	short			delta;
	Cell			theCell;

	nextName = firstName;
	count = 0;
	if (nextName != nil) {
		do {
			count++;
			nextName = (*nextName)->link;
		} while (nextName != nil);
	};
	
	delta = count - (*LHandle)->dataBounds.bottom;
	if (delta < 0)			/* need to remove this many cells */
		LDelRow(-delta,0,LHandle);
	else if (delta > 0)		/* need to add this many cells */
		LAddRow(delta,(*LHandle)->dataBounds.bottom,LHandle);
		
	theCell.h = 0;
	theCell.v = count-1;
	do {
		HLock((Handle)firstName);
		LSetCell((*firstName)->title+1, *((*firstName)->title), theCell, LHandle);
		HUnlock((Handle)firstName);
		nextName = (*firstName)->link;
		DisposHandle((Handle)firstName);
		firstName = nextName;
		theCell.v--;
	} while (firstName != nil);
	namesChanged = false;
};


pascal void MFDrawList(theWindow, item)
	WindowPtr	theWindow;
	short		item;
{
#define ListItem 11

	short	kind;
	Handle	h;
	Rect	r;

	if (item == ListItem) {
		GetDItem(theWindow,ListItem,&kind,&h,&r);
		EraseRect(&r);
		FrameRect(&r);
		LUpdate(theWindow->visRgn,LHandle);
		if (namesChanged)
			SetNames();
	};
};

pascal Boolean MFModalFilter(theDialog, theEvent, itemHit)
	DialogPtr	theDialog;
	EventRecord	*theEvent;
	short		*itemHit;
{

#pragma unused (theDialog, itemHit)

	Boolean	doubleClick;
	Point	localPt;
	
	if (theEvent->what == mouseDown) {
		localPt = theEvent->where;
		GlobalToLocal(&localPt);
		doubleClick = LClick(localPt,theEvent->modifiers,LHandle);
		/* should check for double clicks here */
	};
	return(false);
};


pascal short MultiFileHook(item, dPtr)
	short		item;
	DialogPtr	dPtr;
{
#define ListItem 11

	short	kind;
	Handle	h;
	Rect	r;
	Rect	rView, dataBounds;

	switch (item) {
		case firstTime:
			GetDItem(dPtr,ListItem,&kind,&h,&r);
			SetDItem(dPtr,ListItem,kind,(Handle)MFDrawList,&r);
			rView = r;
			rView.right -= 15;
			InsetRect(&rView,1,1);
			dataBounds.top = 0;
			dataBounds.left = 0;
			dataBounds.bottom = 0;
			dataBounds.right = 1;
			LHandle = LNew(&rView,	/* position in window */
				&dataBounds,		/* initial size of array */
				gZeroPt,			/* cell size (0 = default) */
				0L,					/* resource ID of LDEF */
				dPtr,				/* window pointer */
				true,				/* drawit */
				false,				/* has grow */
				false,				/* scrollHoriz */
				true);				/* scrollVert */
			break;
			
		case 100:
			if (needToUpdate) {
				GetDItem(dPtr,ListItem,&kind,&h,&r);
				InvalRect(&r);
				needToUpdate = false;
			};
			break;
			
		case getOpen:
		case getCancel:
			LDispose(LHandle);	/* Our linked list of names was already disposed */
								/* of in SetNames, so nothing else to do here... */
			break;
	};
	return(item);
};

pascal Boolean ScoopNames(pb)
	ParmBlkPtr	pb;
{
	StringHolderHdl	nextName;
	
	nextName = (StringHolderHdl)NewHandle(sizeof(StringHolder));
	HLock((Handle)nextName);
	pStrcpy(&(*nextName)->title, pb->fileParam.ioNamePtr);
	(*nextName)->link = firstName;
	firstName = nextName;
	HUnlock((Handle)nextName);
	namesChanged = true;
	needToUpdate = true;

	return(false); /*show everything*/
};

void doMultiFile()
{
	firstName = nil;
	namesChanged = false;
	SFPGetFile(gLocation,	/* location */
		"\pSpace for Rent",	/* vestigial string */
		ScoopNames,			/* fileFilter */
		-1,					/* numtypes; -1 means all */
		&typeList,			/* array to types to show */
		MultiFileHook,		/* dlgHook */
		&reply,				/* record for returned values */
		rMultiFileDLOG,		/* ID of Custom Dialog */
		MFModalFilter);		/* ModalDialog filterProc */
};


/** doListsNPutFile ***********************************************************/
/*
/*	This sample demonstrates putting a List Manager list into an SFPPutFile
/*	dialog, describes a problem that can occur by doing so, and shows one
/*	solution to the problem.
/*
/*	Putting another list into a Standard File dialog can be useful for a number
/*	of reasons. One of them was shown above, where we attempted to replace
/*	Standard File's list with one of our own. However, the failure we met with
/*	there forces us to look for another solution. This solution could possibly
/*	take place in the form of TWO lists: one supplied by Standard file, and the
/*	other handled by us to hold the multiple file names.
/*
/*	While the actual logistics of putting the selected filenames into the list
/*	as they are selected is left to you, the Programmer, I do show how to
/*	create and handle the list.
/*
/*	While *I* can't think of a reason for wanting to put a second list into
/*	a PutFile dialog, it is possible that you may wish to. However, there is
/*	a subtle problem that arises if you do: that of determining when to
/*	dispose of the list. In the MultiFile example above, we were perfectly
/*	justified in disposing of the list when the user clicked on Open or
/*	Cancel. However, in the following example, if were to dispose of the list
/*	when the user clicked on Save, we may hit a snag. It is possible for the
/*	user to specify the name of a file that alreay exists. When that happens,
/*	s/he is presented with another dialog that asks if they are sure of what
/*	they are doing. At that point, the user could press Cancel, and return us
/*	to the PutFile dialog, SANS our 2nd list! Neither can we defer disposing
/*	of list until Standard File is done and returns to our application, as the
/*	Dialog box we were using is gone, and the List Manager try to perform an
/*	InvalRect on a non-existant window when it erases its list.
/*
/*	There are two solutions to this problem:
/*
/*		1) Implement the list as a Custom Control. Then, when it somes time to
/*			close the dialog box, the custom control will get called with a
/*			dispCntl message. It can take that opportunity to call LDispose.
/*
/*			This technique is not presented here, as it is more appropriate
/*			for a Custom Control Sample program.  However, it is the suggested
/*			way to proceed.
/*
/*		2) Ensure that the confirmation dialog box never comes up under Standard
/*			File's control. By calling it ourself, we know what the user chooses.
/*			If the user presses OK, we delete the offending file, dispose of our
/*			list, and return to Standard File. If the user presses Cancel, we
/*			change the click on the Save button into a NULL event. This is the
/*			algorithm we use below.
/*
/*			There is a major disadvantage with this approach, however. With it,
/*			we cannot implement a safe saving procedure. Normally, the best way
/*			to save a file (disk space permitting), is to save the data to a
/*			temporary file, delete the original, and then rename the temporary
/*			file to that of the original. However, with approach #2, the file
/*			is deleted well before it is safe to do so.
/*
/*			This routine *could* be reworked to avoid this problem. For instance,
/*			instead of being deleted, the offending file could be renamed to
/*			something else or moved to another directory.
/*
/******************************************************************************/

pascal void LNPFDrawList(theWindow, item)
	WindowPtr	theWindow;
	short		item;
{
#define ListItem 9

	short	kind;
	Handle	h;
	Rect	r;

	if (item == ListItem) {
		GetDItem(theWindow,ListItem,&kind,&h,&r);
		FrameRect(&r);
		LUpdate(theWindow->visRgn,LHandle);
	};
};

pascal Boolean LNPFModalFilter(theDialog, theEvent, itemHit)
	DialogPtr	theDialog;
	EventRecord	*theEvent;
	short		*itemHit;
{
#pragma unused(theDialog, itemHit)
	Point	localPt;

	if (theEvent->what == mouseDown) {
		localPt = theEvent->where;
		GlobalToLocal(&localPt);
		LClick(localPt,theEvent->modifiers,LHandle);
	};
	return(false);
};


pascal short ListsNPutFileHook(item, dPtr)
	short		item;
	DialogPtr	dPtr;
{
#define ListItem 9
#define ExistingFileALRT -3996

	short		kind;
	Handle		h;
	Rect		r;
	Rect		rView, dataBounds;
	Str255		Entry;
	Cell		theCell;
	short		choice;
	FInfo		fndrInfo;
	WDPBRec		WDRec;
	AlertTHndl	AlertHandle;
	Point		dLoc;
	Rect		*tPtr;

	switch (item) {
		case firstTime:
			GetDItem(dPtr,ListItem,&kind,&h,&r);
			SetDItem(dPtr,ListItem,kind,(Handle)LNPFDrawList,&r);
			rView = r;
			rView.right = rView.right - 15;
			InsetRect(&rView,1,1);
			dataBounds.top = 0;
			dataBounds.left = 0;
			dataBounds.bottom = 0;
			dataBounds.right = 1;
			LHandle = LNew(&rView,	/* position in window */
				&dataBounds,		/* initial size of array */
				gZeroPt,			/* cell size (0== default) */
				0L,					/* resource ID of LDEF */
				dPtr,				/* window pointer */
				true,				/* drawit */
				false,				/* has grow */
				false,				/* scrollHoriz */
				true);				/* scrollVert */
			theCell = gZeroPt;
			do {
				GetIndString(&Entry,rStrList,theCell.v + 1);
				if (*Entry != 0) {
					LAddRow(1,(*LHandle)->dataBounds.bottom,LHandle);
					LSetCell(Entry+1,*Entry,theCell,LHandle);
				};
				theCell.v += 1;
			} while (*Entry != 0);
			break;

		case putSave:
			WDRec.ioNamePtr = nil;
			WDRec.ioVRefNum = - *(short *)SFSaveDisk;
			WDRec.ioWDProcID = 'ERIK';
			WDRec.ioWDDirID = *(long *)CurDirStore;
			PBOpenWD(&WDRec,false);

			err = GetFInfo(&reply.fName,WDRec.ioVRefNum,&fndrInfo);
			if (err == noErr) {
				ParamText(&reply.fName,nil,nil,nil);

				/* Before bringing up the Alert that asks for confirmation, we		*/
				/* have to relocate it. To start with, the window is located at		*/
				/* 0,0 in the Window Manager port. To move the Alert Window, read	*/
				/* the resource into memory and change the boundsRect field so		*/
				/* that its topleft is at 12,100 within Standard File's dialog.		*/
	
				AlertHandle = (AlertTHndl)GetResource('ALRT',ExistingFileALRT);
				dLoc = TopLeft(dPtr->portRect);		/* get global location of SF's */
				LocalToGlobal(&dLoc);				/* dialog box. */
				tPtr = &((*AlertHandle)->boundsRect);
				tPtr->right		-= tPtr->left;	/* get width and height of the Alert */
				tPtr->bottom	-= tPtr->top;	/* into botRight. */
				tPtr->left		= dLoc.h + 12;	/* Change Alert.TopLeft to SF.Dlog.TopLeft */
				tPtr->top		= dLoc.v + 100;	/* plus 12,100. */
				tPtr->right		+= tPtr->left;	/* Adjust Alert.BotRight accordingly. */
				tPtr->bottom	+= tPtr->top;
				choice = Alert(ExistingFileALRT,nil);
				if (choice == cancel) {  /* the OK button is in the Cancel slot (item #2) */
					FSDelete(&reply.fName,WDRec.ioVRefNum);
					LDispose(LHandle);
				} else {
					return(100);	/* Change "Save" into null event */
				};
			}
			break;
		case putCancel:
			LDispose(LHandle);
			break;
	}; /* switch */
	return(item);
};


void doListsNPutFile()
{
	short	vRefNum;

	GetVol(nil,&vRefNum);
	Create("\pDoug",vRefNum,'KAAR','APPL');

	SFPPutFile(gLocation,		/* location */
		"\pSave document as:",	/* prompt string */
		"\pDoug",				/* original name */
		ListsNPutFileHook,		/* dlgHook */
		&reply,					/* record for returned values */
		rListsNPutFileDLOG,		/* ID of custom Dialog */
		LNPFModalFilter);		/* ModalDialog filterProc */
};


/** doPutOptions **************************************************************/
/*
/*	Apple's suggested options box - A modest proposal.
/*
/*	With the proliferation of applications and different file types, many
/*	developers are allowing the user to select from various file formats they
/*	want to save their data as. In an effort to maintain consistancy among
/*	all applications in this respect, we are presenting a sample interface for
/*	letting the user select the file type they want.
/*
/*	This is done by adding 2 items to the PutFile dialog box. One is a text
/*	item that displays the file format that the file will be saved as. The
/*	second is an "OptionsÉ" button that brings up another dialog box. This
/*	dialog box conatins a series of radio buttons next to the names of the
/*	various file formats supported by the application. Also in the second
/*	dialog box are OK and Cancel buttons. After a selection has been made
/*	by the user, the text in the PutFile dialog is updated accordingly.
/*
/******************************************************************************/

void SetFormatString(number, theDialog)
	short		number;
	DialogPtr	theDialog;
{
#pragma unused(number)
#define FormatString 10
	short	kind;
	Handle	h;
	Rect	r;
	Str255	title;

	GetDItem(OptionsDPtr,OptionNumber,&kind,&h,&r);
	GetCTitle((ControlHandle)h,&title);
	GetDItem(theDialog,FormatString,&kind,&h,&r);
	SetIText(h,&title);
};


pascal DrawFrame(theWindow, itemNo)
	WindowPtr	theWindow;
	short		itemNo;
{
	short		kind;
	Handle		h;
	Rect		r;
	PenState	ps;

	GetDItem(theWindow,itemNo,&kind,&h,&r);
	GetPenState(&ps);
	PenSize(2,2);
	FrameRect(&r);
	SetPenState(&ps);
};

void doOptionsDialog(item)
	short	*item;
{
	short	newItem;
	short	itemHit;

	newItem = OptionNumber;
	SelectWindow(OptionsDPtr);
	ShowWindow(OptionsDPtr);
	do {
		ModalDialog(nil,&itemHit);
		if ((itemHit != newItem) && (itemHit > 2)) {
			SetRadioButton(OptionsDPtr,newItem,BTNOFF);
			SetRadioButton(OptionsDPtr,itemHit,BTNON);
			newItem = itemHit;
		}
	} while ((itemHit != ok) && (itemHit != cancel));
	HideWindow(OptionsDPtr);
	if (itemHit == ok)
		*item = newItem;
};

pascal short PutOptionsHook(item, theDialog)
	short		item;
	DialogPtr	theDialog;
{
#define FirstRadioButton 3
#define OptionsButton 9
#define FrameItem 10

	short	kind;
	Handle	h;
	Rect	r;

	switch (item) {
		case firstTime:
			OptionNumber = FirstRadioButton;
			SetRadioButton(OptionsDPtr,OptionNumber,BTNON);
			SetFormatString(OptionNumber,theDialog);
			GetDItem(OptionsDPtr,FrameItem,&kind,&h,&r);
			SetDItem(OptionsDPtr,FrameItem,kind,(Handle)DrawFrame,&r);
			break;
		case OptionsButton:
			doOptionsDialog(&OptionNumber);
			SetFormatString(OptionNumber,theDialog);
			break;
	};
	return(item);
};


void doPutOptions()
{
	OptionsDPtr = GetNewDialog(rOptionsSubDLOG,nil,(WindowPtr) -1L);
	SFPPutFile(gLocation,		/* location */
		"\pSave document as:",	/* prompt string */
		"\pDoug",				/* original name */
		PutOptionsHook,			/* dlgHook */
		&reply,					/* record for returned values */
		rOptionsDLOG,			/* ID of custom Dialog */
		nil);					/* ModalDialog filterProc */
	DisposDialog(OptionsDPtr);
};

/** doIdleUpdates *************************************************************/
/*
/*	There is a problem that Standard File has with updates pending in
/*	background windows in the current application partition.
/*
/*	Standard File calls ModalDialog at the heart of its Main Event Loop.
/*	ModalDialog calls GetNextEvent, and then calls a filterProc internal
/*	to Standard File. This filterProc performs some processing on the event,
/*	(like handling hits on the filename list, the Current Directory button, and
/*	the Disk Name Icon), and then calls the filterProc specified in SFPxxxFile
/*	calls.
/*
/*	Another of the things that the internal filterProc does is look for NULL
/*	events. When one is found, the filterProc returns 100 as the "itemHit".
/*	This forces ModalDialog to return to SF, which in turn can now call
/*	the dlgHook with the bogus 100 item number, indicating that idle time
/*	processing can be performed.
/*
/*	The problem occurs when there are updates pending in windows open in
/*	the current application's partition. These updates will 'clog' the event
/*	queue. When ModalDialog calls GetNextEvent, it will get the update
/*	event. However, there is no way for ModalDialog to respond to it. It
/*	therefore passes the event off to the filterProc, which is SF's internal
/*	one. This filterProc doesn't know how to handle it either. Normally,
/*	the event would be ignored at this point, and the update event would be
/*	unresolved. Control returns back to ModalDialog, which calls GetNextEvent
/*	again, and gets the same udpate event back. NULL events will not get
/*	returned by GetNextEvent, and, hence, the dlgHook will never get called
/*	with itemHi==100.
/*
/*	This situation can be solved by providing your own filterProc
/*	procedure to be called after SF's internal filterProc. It will be this
/*	routine's responsibility to check for update events, and handle them
/*	appropriately. Usually, this would mean that the update procedure
/*	within the application would be called, and the update could be cleared.
/*	However, the filterProc could also just handle update events in the same
/*	way that Standard File's filterProc handles NULL events. This is done
/*	by returning 100 in the ItemHit parameter and TRUE as the function result.
/*	This is the approach taken by the sample below.
/*
/******************************************************************************/


/* This is our dlgHook routine. All it does is wait around for NULL events	*/
/* and draws the current time when it gets one. Note that this routine will	*/
/* NOT get called with ite==100 if we did not have the filter procedure		*/
/* below. */

pascal short IdleTimeHook(item, dlg)
	short		item;
	DialogPtr	dlg;
{
#pragma unused(dlg)
	unsigned long	dateTime;
	Str255	result;

	if (item == 100) {
		TextMode(srcCopy);
		MoveTo(250,15);
		GetDateTime(&dateTime);
		IUTimeString(dateTime, /* wantSecond == */ true, &result);
		pStrcat(&result,"\p  "); /*pad with spaces to erase longer strings*/
		DrawString(&result);
	};
	return(item);
};

pascal Boolean filter(dlg, evt, itemHit)
	DialogPtr	dlg;
	EventRecord	*evt;
	short		*itemHit;
{
	/* If we get an update event for a window other than the dialog box, change */
	/* it to a NULL event, and tell ModalDialog that we handled it. */

	if ((evt->what == updateEvt) && (evt->message != dlg)) {
		*itemHit = 100;
		return(true);
	};
	return(false);
};

void doIdleUpdates()
{
	Rect		r;
	WindowPtr	tPtr;

	/* Create a window with a non-empty update region. */

	r.top = 40; r.left = 10; r.bottom = 340; r.right = 500;

	tPtr = NewWindow(nil,		/* Window storage */
		&r,						/* bounding rectangle */
		"\pUn-updated Window",
		true,					/* is visible */
		documentProc,			/* procID */
		(WindowPtr)-1L,			/* bring it up on top */
		true,					/* has goaway */
		0L);					/* refcon */

	SFPGetFile(gLocation,		/* location */
		"\pSpace for Rent",		/* vestigial string */
		nil,					/* fileFilter */
		-1,						/* numtypes; -1 means all */
		&typeList,				/* array to types to show */
		IdleTimeHook,			/* dlgHook */
		&reply,					/* record for returned values */
		-4000,					/* ID of Normal Dialog */
		filter);				/* ModalDialog filterProc */

	DisposeWindow(tPtr);
};


/** doForceDirectory **********************************************************/
/*
/*	This is a quick sample that shows how to set the initial directory that
/*	Standard File comes up with. Basically, this is done by storing apporpriate
/*	values into SFSaveDisk and CurDirStore. In this example, I force the
/*	directory to be the Blessed Folder as specified by SysEnvirons.
/*
/******************************************************************************/


void doForceDirectory()
{
	WDPBRec	pb;
	Str255	s;

	pb.ioNamePtr = nil;
	pb.ioVRefNum = theWorld.sysVRefNum;
	pb.ioWDIndex = 0;
	pb.ioWDProcID = 0;
	PBGetWDInfo(&pb,false);

	*(long *)CurDirStore = pb.ioWDDirID;
	*(short *)SFSaveDisk = -pb.ioWDVRefNum;

	SFGetFile(gLocation,		/* location */
		"\pSpace for Rent",		/* vestigial string */
		nil,					/* fileFilter */
		-1,						/* numtypes; -1 means all */
		&typeList,				/* array to types to show */
		nil,					/* dlgHook */
		&reply);				/* record for returned values */

	if (reply.good )
		ShowSelection(pStrcat(PathNameFromWD(reply.vRefNum,&s),reply.fName));
	else
		ShowCancelled();
};
